package simulation;

import static common.Print.println;

import java.util.Random;

import common.Tuple;

public class Lattice {
	int N = 0;
	double J = 0;
	double B = 0;
	double T = 0;
	int L = 0;
	Tuple[] iter;
	int dimension = 0;

	Tuple[][][] th_neighbors1;
	Tuple[][][][] th_neighbors2;

	Tuple[][][] tv_neighbors1;
	Tuple[][][][] tv_neighbors2;

	int[][] lattice1;
	int[][][] lattice2;

	int[][] cluster1;
	int[][][] cluster2;

	String method = "Glauber";
	// final String method = "Metropolis";
	Random rand = new Random();
	int numberSites = 0;
	double LD = 0;
	double dT = 0;

	// if we use wolf Agorithm we use same probability
	// for each bond to add. We should calculate it once and use it every time

	double h_prob = 0;
	double v_prob = 0;

	// When we claculate Energy_B we calculate exp too many times. I'll
	// introduce two temporary variables

	double Eb_plus_one = 0;
	double Eb_minus_one = 0;

	// another temporary variable
	int h_neighbors_length = 0;
	int v_neighbors_length = 0;

	public Lattice(int N, double J, double B, double T, int L,
			int[][] h_neighbors, String method) {

		this.dimension = (h_neighbors[0]).length;// number of the spatial
													// dimension
													// of
													// our
													// lattice
		this.h_neighbors_length = h_neighbors.length;
		this.v_neighbors_length = 2;

		this.N = N;
		this.J = J;
		this.T = T;
		this.L = L;
		this.B = B;

		this.method = method;

		int[][] v_neighbors = { { -1 }, { 1 } };

		Random rand = new Random();

		this.dT = 1 / (this.L * this.T);

		if (B != 0) {
			this.LD = -0.5 * Math.log(Math.tanh(this.B * this.dT));
		}

		// this variables for Wolf algorithm
		this.h_prob = 1 - Math.exp(-2 * this.J * this.dT);
		this.v_prob = 1 - Math.exp(-2 * this.LD);

		// variables for Energy_B calculation
		this.Eb_plus_one = Math.exp(-2 * this.LD);
		this.Eb_minus_one = Math.exp(2 * this.LD);

		// work with 1d chain in transverse field
		if (this.dimension == 1) {
			this.numberSites = this.L * this.N;

			this.iter = new Tuple[this.numberSites];
			// fill iterator array
			for (int t = 0; t < this.L; t++) {
				for (int x = 0; x < this.N; x++) {
					this.iter[t * this.N + x] = new Tuple(t, x);
				}
			}
			// fill neighbors array and fill lattice with random numbers
			this.th_neighbors1 = new Tuple[this.L][this.N][this.h_neighbors_length];
			this.tv_neighbors1 = new Tuple[this.L][this.N][this.v_neighbors_length];// we
			// know
			// that
			// we
			// have neighbor
			// one layer
			// higher and
			// one lower

			int tx = 0;
			int tt = 0;

			this.lattice1 = new int[this.L][this.N];
			this.cluster1 = new int[this.L][this.N];

			for (Tuple site : this.iter) {
				// fill neighbors array
				for (int tn = 0; tn < this.h_neighbors_length; tn++) {
					// tx = site.getX() + h_neighbors[tn][0];
					tx = site.x + h_neighbors[tn][0];
					if (tx == this.N) {
						tx = 0;
					} else if (tx == -1) {
						tx = this.N - 1;
					}
					// this.th_neighbors1[site.getT()][site.getX()][tn] = new
					// Tuple(site.getT(), tx);
					this.th_neighbors1[site.t][site.x][tn] = new Tuple(site.t,
							tx);
					// fill lattice with random numbers

					// this.lattice1[site.getT()][site.getX()] = 2 *
					// rand.nextInt(2) - 1;
					this.lattice1[site.t][site.x] = 2 * rand.nextInt(2) - 1;
				}
				if (B != 0) {
					for (int tv = 0; tv < this.v_neighbors_length; tv++) {

						if (tt == this.L) {
							tt = 0;
						} else if (tt == -1) {
							tt = this.L - 1;
						}
						// this.tv_neighbors1[site.getT()][site.getX()][tv] =
						// new Tuple(tt, site.getX());
						this.tv_neighbors1[site.t][site.x][tv] = new Tuple(tt,
								site.x);
						// fill lattice with random numbers
					}
				}
				// this.lattice1[site.getT()][site.getX()] = 2 * rand.nextInt(2)
				// - 1;
				this.lattice1[site.t][site.x] = 2 * rand.nextInt(2) - 1;
				// this.cluster1[site.getT()][site.getX()] = 0;
				this.cluster1[site.t][site.x] = 0;
			}
		}
		// work with 2d lattice in transverse field
		else if (this.dimension == 2) {
			this.numberSites = this.L * this.N * this.N;

			this.iter = new Tuple[this.numberSites];
			// fill iterator array
			for (int t = 0; t < this.L; t++) {
				for (int x = 0; x < this.N; x++) {
					for (int y = 0; y < this.N; y++) {
						this.iter[t * this.N * this.N + y * this.N + x] = new Tuple(
								t, x, y);
					}
				}
			}
			// fill neighbors array and fill lattice with random numbers
			this.th_neighbors2 = new Tuple[this.L][this.N][this.N][h_neighbors_length];
			this.tv_neighbors2 = new Tuple[this.L][this.N][this.N][v_neighbors_length];

			int tt = 0;
			int tx = 0;
			int ty = 0;

			this.lattice2 = new int[this.L][this.N][this.N];
			this.cluster2 = new int[this.L][this.N][this.N];

			for (Tuple site : this.iter) {
				// fill neighbors array
				for (int tn = 0; tn < h_neighbors_length; tn++) {
					// tx = site.getX() + h_neighbors[tn][0];
					tx = site.x + h_neighbors[tn][0];
					if (tx == this.N) {
						tx = 0;
					} else if (tx == -1) {
						tx = this.N - 1;
					}
					// ty = site.getY() + h_neighbors[tn][1];
					ty = site.y + h_neighbors[tn][1];
					if (ty == this.N) {
						ty = 0;
					} else if (ty == -1) {
						ty = this.N - 1;
					}

					// this.th_neighbors2[site.getT()][site.getX()][site.getY()][tn]
					// = new Tuple(site.getT(), tx, ty);
					this.th_neighbors2[site.t][site.x][site.y][tn] = new Tuple(
							site.t, tx, ty);

				}
				if (B != 0) {
					for (int tv = 0; tv < v_neighbors_length; tv++) {
						// tt = site.getT() + v_neighbors[tv][0];
						tt = site.t + v_neighbors[tv][0];
						// tt = site.getT() + v_neighbors[tv][0];
						tt = site.t + v_neighbors[tv][0];
						// println("tt: " + tt);
						// println("site.getT(): " + site.getT());

						if (tt == this.L) {
							tt = 0;
						} else if (tt == -1) {
							tt = this.L - 1;
						}
						// println("t: " + site.getT() + ", x: " + site.getX() +
						// ", y: " + site.getY());
						// this.tv_neighbors2[site.getT()][site.getX()][site.getY()][tv]
						// = new Tuple(tt, site.getX(), site.getY());
						this.tv_neighbors2[site.t][site.x][site.y][tv] = new Tuple(
								tt, site.x, site.y);
						// fill lattice with random numbers

					}
				}

				// this.lattice2[site.getT()][site.getX()][site.getY()] = 2 *
				// rand.nextInt(2) - 1;
				this.lattice2[site.t][site.x][site.y] = 2 * rand.nextInt(2) - 1;
				// this.cluster2[site.getT()][site.getX()][site.getY()] = 0;
				this.cluster2[site.t][site.x][site.y] = 0;

			}

		}
	}

	// function finds energy of the lattice
	public double Energy_J() {
		int result = 0;
		for (Tuple site : this.iter) {
			result -= ValueGet(site) * Sum_H_Neighbors(site);
		}
		return this.J * result / 2;
	}

	public double Energy_B() {
		double result = 0;

		if (this.B != 0) {
			for (Tuple site : this.iter) {
				// println();
				// site.print();
				// V_NeighborDown(site).print();

				// we calculate exponent too many times. Lets try optimize it.

				int temp = ValueGet(site) * ValueGet(V_NeighborDown(site));// temp
																			// can
																			// be
																			// +1
																			// or
																			// -1

				if (temp == 1) {
					result -= this.Eb_plus_one;
				} else if (temp == -1) {
					result -= this.Eb_minus_one;
				}

				// result -= Math.exp(-2 * this.LD * ValueGet(site)*
				// ValueGet(V_NeighborDown(site)));
			}
			return this.B * result;
		}
		return result;
	}

	public void ClusterInit() {

		for (Tuple site : this.iter) {
			ClusterSet(site, 0);
		}
	}

	public double Energy_B_test() {
		double result = 0;
		if (this.B != 0) {
			for (Tuple site : this.iter) {
				// println();
				// site.print();
				// V_NeighborDown(site).print();
				result -= Math.exp(-2 * this.LD * ValueGet(site)
						* ValueGet(V_NeighborUp(site)));
			}
			return this.B * result;
		}
		return result;

	}

	// returns site from higher layer
	public Tuple V_NeighborUp(Tuple site) {
		return V_Neighbors(site)[1];
	}

	// returns site from higher layer
	public Tuple V_NeighborDown(Tuple site) {
		return V_Neighbors(site)[0];
	}

	// function finds magnetisation of the lattice
	public int M() {
		int result = 0;
		for (Tuple site : this.iter) {
			result += ValueGet(site);
		}
		return result;
	}

	public double dE_J(Tuple site) {
		return 2 * this.J * ValueGet(site) * Sum_H_Neighbors(site);
	}

	public double dS(Tuple site) {
		if (B != 0) {
			return dE_J(site) * this.dT + 2 * this.LD * ValueGet(site)
					* Sum_V_Neighbors(site);
		}
		return dE_J(site) * this.dT;
	}

	Tuple[] H_Neighbors(Tuple site) {
		if (this.dimension == 2) {
			// Tuple[] result = new
			// Tuple[this.th_neighbors2[site.getT()][site.getX()][site.getY()].length];
			// Tuple[] result = new
			// Tuple[this.th_neighbors2[site.t][site.x][site.y].length];
			Tuple[] result = new Tuple[this.h_neighbors_length];
			// for (int tn = 0; tn <
			// this.th_neighbors2[site.getT()][site.getX()][site.getY()].length;
			// tn++) {

			// for (int tn = 0; tn <
			// this.th_neighbors2[site.t][site.x][site.y].length; tn++) {
			for (int tn = 0; tn < this.h_neighbors_length; tn++) {
				// result[tn] =
				// this.th_neighbors2[site.getT()][site.getX()][site.getY()][tn];
				result[tn] = this.th_neighbors2[site.t][site.x][site.y][tn];
			}
			return result;
		} else if (this.dimension == 1) {
			// Tuple[] result = new
			// Tuple[this.th_neighbors1[site.getT()][site.getX()].length];
			// Tuple[] result = new
			// Tuple[this.th_neighbors1[site.t][site.x].length];
			Tuple[] result = new Tuple[this.h_neighbors_length];
			// for (int tn = 0; tn <
			// this.th_neighbors1[site.getT()][site.getX()].length; tn++) {
			// for (int tn = 0; tn < this.th_neighbors1[site.t][site.x].length;
			// tn++) {
			for (int tn = 0; tn < this.h_neighbors_length; tn++) {
				// result[tn] =
				// this.th_neighbors1[site.getT()][site.getX()][tn];
				result[tn] = this.th_neighbors1[site.t][site.x][tn];
			}
			return result;
		}
		// here I need throw exception
		else {
			println("We should not be here, H_neighbors");
			// Tuple[] result = new
			// Tuple[this.th_neighbors1[site.getT()][site.getX()].length];
			// Tuple[] result = new
			// Tuple[this.th_neighbors1[site.t][site.x].length];
			Tuple[] result = new Tuple[this.h_neighbors_length];
			return result;
		}
	}

	Tuple[] V_Neighbors(Tuple site) {
		if (this.dimension == 2) {
			// int lenVneighbors =
			// (this.tv_neighbors2[site.getT()][site.getX()][site.getY()]).length;
			// int lenVneighbors =
			// (this.tv_neighbors2[site.t][site.x][site.y]).length;
			Tuple[] result = new Tuple[this.v_neighbors_length];
			for (int tv = 0; tv < this.v_neighbors_length; tv++) {
				// result[tv] =
				// this.tv_neighbors2[site.getT()][site.getX()][site.getY()][tv];
				result[tv] = this.tv_neighbors2[site.t][site.x][site.y][tv];
			}
			return result;
		} else if (this.dimension == 1) {
			// Tuple[] result = new
			// Tuple[this.tv_neighbors1[site.getT()][site.getX()].length];
			// Tuple[] result = new
			// Tuple[this.tv_neighbors1[site.t][site.x].length];
			Tuple[] result = new Tuple[this.v_neighbors_length];
			// for (int tv = 0; tv <
			// this.tv_neighbors1[site.getT()][site.getX()].length; tv++) {
			// for (int tv = 0; tv < this.tv_neighbors1[site.t][site.x].length;
			// tv++) {
			for (int tv = 0; tv < this.v_neighbors_length; tv++) {
				// result[tv] =
				// this.tv_neighbors1[site.getT()][site.getX()][tv];
				result[tv] = this.tv_neighbors1[site.t][site.x][tv];
			}
			return result;
		}
		// here I need throw exception
		else {
			println("We should not be here, V_neighbors");
			// Tuple[] result = new
			// Tuple[this.tv_neighbors1[site.getT()][site.getX()].length];
			// Tuple[] result = new
			// Tuple[this.tv_neighbors1[site.t][site.x].length];
			Tuple[] result = new Tuple[this.v_neighbors_length];
			return result;
		}
	}

	int Sum_H_Neighbors(Tuple site) {
		int result = 0;
		for (Tuple neighbor : H_Neighbors(site)) {
			result += ValueGet(neighbor);
		}
		return result;
	}

	int Sum_V_Neighbors(Tuple site) {
		int result = 0;
		for (Tuple neighbor : V_Neighbors(site)) {
			result += ValueGet(neighbor);
		}
		return result;
	}

	// returns value that is on the lattice at current site
	public int ValueGet(Tuple site) {
		if (this.dimension == 2) {
			// return this.lattice2[site.getT()][site.getX()][site.getY()];
			return this.lattice2[site.t][site.x][site.y];
		} else if (this.dimension == 1) {
			// return this.lattice1[site.getT()][site.getX()];
			return this.lattice1[site.t][site.x];
		}
		println("We should not be here, ValueGet");
		return 0;
	}

	public void ValueSet(Tuple site, int value) {
		if (this.dimension == 2) {
			// this.lattice2[site.getT()][site.getX()][site.getY()] = value;
			this.lattice2[site.t][site.x][site.y] = value;
		} else if (this.dimension == 1) {
			// this.lattice1[site.getT()][site.getX()] = value;
			this.lattice1[site.t][site.x] = value;
		}
	}

	public void ClusterSet(Tuple site, int value) {
		if (this.dimension == 2) {
			// this.cluster2[site.getT()][site.getX()][site.getY()] = value;
			this.cluster2[site.t][site.x][site.y] = value;
		} else if (this.dimension == 1) {
			// this.cluster1[site.getT()][site.getX()] = value;
			this.cluster1[site.t][site.x] = value;
		}
	}

	public int ClusterGet(Tuple site) {
		if (this.dimension == 2) {
			// return this.cluster2[site.getT()][site.getX()][site.getY()];
			return this.cluster2[site.t][site.x][site.y];
		} else if (this.dimension == 1) {
			// return this.cluster1[site.getT()][site.getX()];
			return this.cluster1[site.t][site.x];
		}
		println("We should not be here, ClusterGet");
		return 0;
	}

	public void update() {
		/*
		 * Here most magic is stored There are bunch of different update
		 * tecniques: Metropolis Glauber Wolf Temporal Cluster etc
		 * 
		 * all of them can be operated choosing spin randomly or going through
		 * them in some order
		 * 
		 * Although it is a bit slower I'll choose spin's randomly
		 */
		if ((this.method.equals("Wolf")) && (this.J > 0)) {// We work with
															// ferromagnatic, so
															// we can use
			// Wolf algorithm
			Tuple site = RandomSpin();
			ClusterInit();// put all cluster values to zero

			// add spin to cluster
			ClusterSet(site, 1);

			int ClusterSpin = ValueGet(site);// spin of the cluster the we try
			// to grow
			GrowCluster(site, ClusterSpin);

		}

		else if (this.method.equals("Metropolis")) {
			for (int i = 0; i < this.numberSites; i++) {
				Tuple site = RandomSpin();
				double tdS = dS(site);

				if (tdS <= 0) {
					SpinFlip(site);
				} else if (rand.nextFloat() < Math.exp(-tdS)) {
					SpinFlip(site);
				}
			}
		} else if (this.method.equals("Glauber")) {
			for (int i = 0; i < this.numberSites; i++) {
				Tuple site = RandomSpin();
				double tdS = dS(site);

				if (rand.nextFloat() < (1 / (1 + Math.exp(tdS)))) {
					SpinFlip(site);
				}
			}
		}
	}

	public void GrowCluster(Tuple site, int ClusterSpin) {
		SpinFlip(site);
		ClusterSet(site, 1);
		for (Tuple h_neighbor : H_Neighbors(site)) {
			if (ValueGet(h_neighbor) == ClusterSpin) {// if spin of the spin we
														// try to add is same as
														// for cluster
				if (ClusterGet(h_neighbor) == 0) {// if spin not in cluster
					// tryAdd_horizontal(h_neighbor, ClusterSpin);
					if (rand.nextDouble() < this.h_prob)// (1 - Math.exp(-2 *
														// this.J * this.dT)))
					{
						GrowCluster(h_neighbor, ClusterSpin);
					}
				}
			}
		}
		if (B != 0) {// we play with vertical neighbors only if we have
						// transverse field
			for (Tuple v_neighbor : V_Neighbors(site)) {
				if (ValueGet(v_neighbor) == ClusterSpin) {// if spin of the spin
															// we
															// try to add is
															// same as
															// for cluster
					if (ClusterGet(v_neighbor) == 0) {// if spin not in cluster
						// tryAdd_vertical(v_neighbor, ClusterSpin);
						if (rand.nextDouble() < this.v_prob) {// (1 -
																// Math.exp(-2 *
																// this.LD))) {
							GrowCluster(v_neighbor, ClusterSpin);
						}
					}
				}
			}
		}
	}

	// public void tryAdd_horizontal(Tuple site, int ClusterSpin) {
	// if (rand.nextDouble() < (1 - Math.exp(-2 * this.J * this.dT))) {
	// GrowCluster(site, ClusterSpin);
	// }
	// }
	//
	// public void tryAdd_vertical(Tuple site, int ClusterSpin) {
	// if (rand.nextDouble() < (1 - Math.exp(-2 * this.LD))) {
	// GrowCluster(site, ClusterSpin);
	// }
	// }

	public void SpinFlip(Tuple site) {
		int newValue = (-1) * ValueGet(site);
		ValueSet(site, newValue);
	}

	// returns random spin
	public Tuple RandomSpin() {
		if (this.dimension == 2) {
			return new Tuple(rand.nextInt(this.L), rand.nextInt(this.N),
					rand.nextInt(this.N));
		} else if (this.dimension == 1) {
			return new Tuple(rand.nextInt(this.L), rand.nextInt(this.N));
		}

		else {
			println("We should not be here, RandomSpin");
			return new Tuple(0, 0);
		}

	}
}
